package com.ldap;

import java.util.Hashtable;
import java.util.Properties;

import javax.naming.AuthenticationException;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.NameAlreadyBoundException;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Active Directory 操作类
 * 
 * @author jliu
 * 
 */
public class ADOperator {
	private final static Log LOG = LogFactory.getLog(ADOperator.class);

	private DirContext adminCtx;

	private static final String CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";

	private static final int UF_ACCOUNTDISABLE = 0x0002;// 帐户禁用

	private static final int UF_PASSWD_NOTREQD = 0x0020;// 密码可以不设（为空）

	// private static final int UF_PASSWD_CANT_CHANGE = 0x0040;// 用户不能更改密码

	private static final int UF_NORMAL_ACCOUNT = 0x0200;// 普通帐户

	private static final int UF_DONT_EXPIRE_PASSWD = 0x10000;// 密码永不过期

	private static final int UF_PASSWORD_EXPIRED = 0x800000;// 用户下次登陆时须更改密码

	private static final String PROTOCOL = "ssl";

	private static String ldap_url; // ldap地址

	private static String auth_type;// 认证类型，一般为simple

	private static String manager_dn; // ldap管理员的dn

	private static String manager_password;// ldap管理员密码

	private static String certficationPath;// JDK中导入的证书路径

	private static String certficationPwd;// 证书密码

	private static String domain; // 域名，包括@符号

	private static String baseUserDN;// 用户的DN基础路径

	private static String enabledLCS; // 是否开户用户的LCS功能

	private static String LCServerDN; // LCS服务器的ldap DN

	static {
		Properties p = new Properties();

		try {
			LOG.info("LDAP 开始读取配置文件 ldap.properties......");

			p.load(ADOperator.class.getResourceAsStream("/ldap.properties"));
			ldap_url = p.getProperty("ldap_url");
			auth_type = p.getProperty("auth_type");
			manager_dn = p.getProperty("manager_dn");
			manager_password = p.getProperty("manager_password");
			domain = "@" + p.getProperty("domain");
			enabledLCS = p.getProperty("enabledLCS");
			LCServerDN = p.getProperty("LCServerDN");
			baseUserDN = p.getProperty("baseUserDN");
			certficationPath = p.getProperty("certficationPath");
			certficationPwd = p.getProperty("certficationPwd");

			LOG.info("LDAP 成功读取配置文件 ldap.properties!");

		} catch (Exception e) {
			LOG.error("LDAP 失败读取配置文件 ldap.properties!");

			throw new RuntimeException("LDAP 失败读取配置文件 ldap.properties!", e);
		}
	}

	/**
	 * 关闭LDAP连接
	 * 
	 * @param dirContext
	 *            DirContext 已连接的LDAP的Context实例
	 */
	private void closeDirContext() {
		try {
			if (adminCtx != null) {
				adminCtx.close();
				adminCtx = null;
			}
		} catch (Exception ex) {
			LOG.error("Not close DirContext !", ex);
		}
	}

	/**
	 * 获取 LDAP 服务器连接的方法
	 * 
	 * @param env
	 *            连接LDAP的连接信息
	 * @return DirContext - LDAP server的连接
	 */
	private void initDirContext() {
		try {
			// 设置系统属性中ssl连接的证书和密码
			System.setProperty("javax.net.ssl.trustStore", certficationPath);
			System.setProperty("javax.net.ssl.trustStorePassword",
					certficationPwd);

			Hashtable<String, String> env = new Hashtable<String, String>();
			env.put(Context.INITIAL_CONTEXT_FACTORY, CONTEXT_FACTORY);
			env.put(Context.PROVIDER_URL, ldap_url);
			env.put(Context.SECURITY_AUTHENTICATION, auth_type);
			env.put(Context.SECURITY_PRINCIPAL, manager_dn);
			env.put(Context.SECURITY_CREDENTIALS, manager_password);
			env.put(Context.SECURITY_PROTOCOL, PROTOCOL);
			// 通过参数连接LDAP/AD
			adminCtx = new InitialDirContext(env);

			if (LOG.isDebugEnabled()) {
				LOG.debug("ldap 连接 AD 成功");
			}

		} catch (NamingException ex) {
			LOG.error("ldap 连接 AD 失败!");
			LOG.error("CONTEXT_FACTORY:" + CONTEXT_FACTORY + ",ldap_url:"
					+ ldap_url + ",auth_type:" + auth_type + ",manager_dn:"
					+ manager_dn + ",manager_password:" + manager_password, ex);

			throw new RuntimeException(ex);
		}
	}

	// ------------------------------------------------------------------------START
	// 所有操作
	// ------------------------------------------------------------------------START
	public ADOperatorResult createUser(ADUserInfo user) {
		ADOperatorResult re = null;
		// ------------------------------------------校验START
		String msg = "";
		if (StringUtils.isBlank(user.getSamAccountName())) {
			msg += "samAccountName(loginId) is required,";
		}
		if (StringUtils.isBlank(user.getOu())) {
			msg += " ou(organization unit) is required,";
		}
		if (StringUtils.isBlank(user.getPassword())) {
			msg += " password is required";
		}
		if (StringUtils.isNotBlank(msg)) {
			re = new ADOperatorResult(false, msg);
			return re;
		}
		// ------------------------------------------校验END

		try {

			if (this.adminCtx == null) {
				this.initDirContext();
			}

			if (StringUtils.isBlank(user.getCn())) {
				user.setCn(user.getSamAccountName());
			}

			String dn = getDN(user.getCn(), user.getOu());// 默认

			if (this.isUserExistByDN(dn)) {

				re = new ADOperatorResult(false, "DN(用户全名,含名称和部门):" + dn
						+ ",已存在!");
				return re;
			}

			Attributes attrs = this.getCommonUserAttrs(user);
			this.addObjclassAttrs(attrs);

			// 控制用户的有效性
			putAttribute(
					attrs,
					"userAccountControl",
					Integer.toString(UF_NORMAL_ACCOUNT | UF_DONT_EXPIRE_PASSWD
							| UF_PASSWD_NOTREQD));

			// 以下cn 顺便生成了 name, cn属性
			adminCtx.createSubcontext(dn, attrs);
			re = new ADOperatorResult(true, "用户" + user.getSamAccountName()
					+ "创建成功!");

		} catch (NameAlreadyBoundException nameAlreadyBoundEx) {
			LOG.error(nameAlreadyBoundEx.getMessage(), nameAlreadyBoundEx);

			re = new ADOperatorResult(false, "loginId(登陆名):"
					+ user.getSamAccountName() + ",已存在!");

		} catch (Exception e) {
			LOG.error(e.getMessage(), e);

			re = new ADOperatorResult(false, e.getMessage());

		} finally {
			this.closeDirContext();
		}

		LOG.info(re.getMsg());
		return re;
	}

	/**
	 * 修改密码需要证书，请确认已经导入了证书JDK中<br>
	 * 修改用户时,用户不存在,先创建该用户
	 * 
	 * @param user
	 * @throws Exception
	 */
	public ADOperatorResult changePassword(ADUserInfo user) {
		ADOperatorResult re = null;
		// ------------------------------------------校验START
		String msg = "";
		if (StringUtils.isBlank(user.getSamAccountName())) {
			msg += "samAccountName(loginId) is required,";
		}
		if (StringUtils.isBlank(user.getPassword())) {
			msg += " password is required";
		}
		if (StringUtils.isNotBlank(msg)) {
			re = new ADOperatorResult(false, msg);
			return re;
		}
		// ------------------------------------------校验END

		try {

			if (this.adminCtx == null) {
				this.initDirContext();
			}

			Attributes attrs = this.findUserPrincipal(user.getSamAccountName());

			if (attrs == null) {
				re = new ADOperatorResult(false, "loginId(登陆名):"
						+ user.getSamAccountName() + ",不存在,请先创建该用户!");

				return re;
			}

			ModificationItem[] mods = new ModificationItem[1];
			String newQuotedPassword = "\"" + user.getPassword() + "\"";
			byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");

			mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
					new BasicAttribute("unicodePwd", newUnicodePassword));

			Attribute distinguishedName = attrs.get("distinguishedName");
			adminCtx.modifyAttributes(distinguishedName.get().toString(), mods);

			re = new ADOperatorResult(true, "用户" + user.getSamAccountName()
					+ "修改密码成功!");

		} catch (Exception e) {
			LOG.error(e.getMessage(), e);
			re = new ADOperatorResult(false, e.getMessage());

		} finally {
			this.closeDirContext();
		}

		LOG.info(re.getMsg());
		return re;
	}

	/**
	 * 修改用户时,用户不存在,先创建该用户
	 * 
	 * @param user
	 * @throws Exception
	 */
	public void modifyUser(ADUserInfo user) throws Exception {
		try {
			if (this.adminCtx == null) {
				this.initDirContext();
			}

			if (!this.isUserExistByDN(user.getSamAccountName())) {
				LOG.info("修改用户时,用户不存在,先创建该用户");
				this.createUser(user);
				return;
			}

			Attributes attrs = this.getCommonUserAttrs(user);
			adminCtx.modifyAttributes(
					getDN(user.getSamAccountName(), baseUserDN),
					DirContext.REPLACE_ATTRIBUTE, attrs);

		} catch (Exception e) {
			throw e;
		} finally {
			this.closeDirContext();
		}

	}

	public ADOperatorResult destroyUserByPrincipal(String principalName) {
		ADOperatorResult re = null;
		// ------------------------------------------校验START
		if (StringUtils.isBlank(principalName)) {
			String msg = "samAccountName(loginId) is required,";

			re = new ADOperatorResult(false, msg);
			return re;
		}

		// ------------------------------------------校验END

		try {

			if (this.adminCtx == null) {
				initDirContext();
			}

			Attributes attrs = this.findUserPrincipal(principalName);

			if (attrs == null) {
				re = new ADOperatorResult(false, "删除用户" + principalName
						+ "失败, 用户不存在!");

				return re;
			}

			Attribute distinguishedName = attrs.get("distinguishedName");

			if (this.isUserExistByPrincipal(principalName)) {
				adminCtx.destroySubcontext(distinguishedName.get().toString());

				re = new ADOperatorResult(true, "删除用户" + principalName + "成功!");
			}

		} catch (NamingException e) {
			LOG.error(e.getMessage(), e);

			re = new ADOperatorResult(false, "删除用户" + principalName + "失败!");

		} finally {
			LOG.info(re.getMsg());
			this.closeDirContext();
		}

		return re;
	}

	public void destroyUserByDN(ADUserInfo user) throws NamingException {
		try {
			if (this.adminCtx == null) {
				initDirContext();
			}

			String dn = getDN(user.getCn(), user.getOu());// 默认

			if (this.isUserExistByDN(dn)) {
				adminCtx.destroySubcontext(this.getDN(dn, baseUserDN));
				// 使用userPrinspalName也可以删除
				// ctx.destroySubcontext(userName + domain)
			}

			LOG.info("成功删除用户" + dn);
		} catch (NamingException e) {
			throw e;
		} finally {
			this.closeDirContext();
		}
	}

	public void prohibitUser(ADUserInfo user) throws NamingException {
		try {
			if (this.adminCtx == null) {
				this.initDirContext();
			}

			String dn = getDN(user.getCn(), user.getOu());// 默认
			if (this.isUserExistByDN(dn)) {
				return;
			}

			ModificationItem[] mods = new ModificationItem[1];
			mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
					new BasicAttribute("userAccountControl",
							Integer.toString(UF_NORMAL_ACCOUNT
									+ UF_PASSWORD_EXPIRED)));
			adminCtx.modifyAttributes(dn, mods);

		} catch (NamingException e) {
			throw e;
		} finally {
			this.closeDirContext();
		}

	}

	// 验证用户是否存在
	public boolean isUserExistByDN(String dn) {
		boolean rtn = true;

		Attributes attrs = findUserDN(dn);
		if (attrs == null) {
			rtn = false;
		}

		return rtn;
	}

	// 验证用户是否存在
	public boolean isUserExistByPrincipal(String principal) {
		boolean rtn = true;

		Attributes attrs = findUserPrincipal(principal);
		if (attrs == null) {
			rtn = false;
		}

		return rtn;
	}

	/**
	 * (独立需要打开连接)<br>
	 * 
	 * 查找用户
	 * 
	 * @param dn
	 * @return
	 */
	public Attributes findUserDN(String dn) {
		Attributes reAttrs = null;
		boolean isThisOpen = false;// 标志是否是本方法里打开的上下文

		try {

			if (this.adminCtx == null) {
				this.initDirContext();
				isThisOpen = true;
			}

			reAttrs = adminCtx.getAttributes(dn);

			LOG.info("AD中用户" + dn + "存在!");
		} catch (NameNotFoundException e) {
			LOG.info("AD中用户" + dn + "不存在!");

		} catch (Exception e) {
			LOG.error("ldap查找AD中的用户出错", e);
			throw new RuntimeException(e);

		} finally {

			if (isThisOpen) { // 如果是本方法打开的上下文则关掉上下文
				this.closeDirContext();
			}
		}

		return reAttrs;
	}

	/**
	 * (独立需要打开连接)<br>
	 * 
	 * 查找用户
	 * 
	 * @param principal
	 * @return
	 */
	public Attributes findUserPrincipal(String principal) {
		Attributes reAttrs = null;
		boolean isThisOpen = false;// 标志是否是本方法里打开的上下文

		try {
			if (this.adminCtx == null) {
				this.initDirContext();
				isThisOpen = true;
			}

			SearchControls searchCons = new SearchControls(); // 搜索控件
			searchCons.setSearchScope(SearchControls.SUBTREE_SCOPE);// SUBTREE_SCOPE
			searchCons.setCountLimit(20000);// 设置最大返回个数
			searchCons.setTimeLimit(60 * 1000);// 超时一分钟

			// 搜索对象,第一个参数为基目录,第二个参数为fiter，fiter格式可类似于(&(CN=*)(DC=*))
			NamingEnumeration<SearchResult> searchResults = adminCtx.search(
					baseUserDN, "(CN=" + principal + ")", searchCons);

			// 找不到则全局找
			if (!searchResults.hasMore()) {
				searchResults = adminCtx.search(baseUserDN, "(CN=*)",
						searchCons);
			}

			// 遍历搜索结果
			while (searchResults.hasMore()) {
				// Use debug to see the attributes
				SearchResult result = searchResults.nextElement();

				// 通过属性名称获取搜索结果对象的属性值
				Attributes attrs = result.getAttributes();

				Attribute userPrincipalName = attrs.get("userPrincipalName");
				if (userPrincipalName != null
						&& StringUtils.equals(userPrincipalName.get()
								.toString(), principal + domain)) {

					reAttrs = attrs;
					LOG.info("AD中用户" + principal + domain + "存在!");
				}
			}

		} catch (Exception e) {
			LOG.error("ldap查找AD中的用户出错", e);

		} finally {

			if (isThisOpen) { // 如果是本方法打开的上下文则关掉上下文
				this.closeDirContext();
			}
		}

		return reAttrs;
	}

	/**
	 * 精确认证<br>
	 * 调用简单认证authenticationSample(), 返回失败后再确定是用户名不存在还是密码错误
	 * 
	 * @param principalName
	 * @param password
	 * @return
	 */
	public ADOperatorResult authentication(String principalName, String password) {
		ADOperatorResult re = null;

		// ------------------------------------------校验START
		String msg = "";
		if (StringUtils.isBlank(principalName)) {
			msg += "samAccountName(loginId) is required,";
		}
		if (StringUtils.isBlank(password)) {
			msg += " password is required";
		}
		if (StringUtils.isNotBlank(msg)) {
			re = new ADOperatorResult(false, msg);
			return re;
		}
		// ------------------------------------------校验END

		re = authenticationSample(principalName, password);

		// ----------------------------------------精确认证START
		if (!re.getStatus()) {

			try {
				// 通过属性名称获取搜索结果对象的属性值
				Attributes attrs = this.findUserPrincipal(principalName);

				if (attrs != null) {
					re.setMsg("该用户为:" + attrs.get("distinguishedName").get()
							+ ", 输入密码错误!");
				} else {
					re.setMsg("用户名不存在!");
				}

			} catch (NamingException namingEx) {
				LOG.error(namingEx.getMessage(), namingEx);

			}

		}
		// ----------------------------------------精确认证END
		LOG.info(re.getMsg());
		return re;
	}

	/**
	 * 简单认证, 返回成功或失败
	 * 
	 * @param principalName
	 * @param password
	 * @return
	 */
	public ADOperatorResult authenticationSample(String principalName,
			String password) {
		DirContext userContext = null;
		ADOperatorResult re = null;

		try {
			System.setProperty("javax.net.ssl.trustStore", certficationPath);
			System.setProperty("javax.net.ssl.trustStorePassword",
					certficationPwd);

			Hashtable<String, String> env = new Hashtable<String, String>();

			env.put(Context.INITIAL_CONTEXT_FACTORY, CONTEXT_FACTORY);
			env.put(Context.PROVIDER_URL, ldap_url);
			env.put(Context.SECURITY_AUTHENTICATION, auth_type);
			env.put(Context.SECURITY_PRINCIPAL, principalName + domain);
			env.put(Context.SECURITY_CREDENTIALS, password);
			env.put(Context.SECURITY_PROTOCOL, PROTOCOL);

			userContext = new InitialDirContext(env);

			if (userContext != null) {
				re = new ADOperatorResult(true, "认证通过!");
			}

		} catch (CommunicationException e) {

			String msg = "没有连接上AD服务器, 认证服务的配置AD的ip或port错误!";
			re = new ADOperatorResult(false, msg);
			LOG.error(msg + e.getMessage(), e);

		} catch (AuthenticationException e) {
			String msg = "用户名或密码错误!";
			re = new ADOperatorResult(false, msg);
			LOG.error(msg + e.getMessage());

		} catch (NamingException e) {
			re = new ADOperatorResult(false, e.getMessage());
			LOG.error(e.getMessage(), e);

		} finally {
			try {
				if (userContext != null) {
					userContext.close();
					userContext = null;
				}
			} catch (NamingException ex) {
				LOG.error("Not close userContext !", ex);
			}
		}

		return re;
	}

	// ------------------------------------------------------------------------END
	// 所有操作
	// ------------------------------------------------------------------------END

	// ------------------------------------------------------------------------START
	// 以下都是私有成员, 工具函数
	// ------------------------------------------------------------------------START

	private String getDN(String cn, String ou) {
		StringBuilder sb = new StringBuilder("cn=" + cn + ",");

		if (StringUtils.isNotBlank(ou)) {
			sb.append("ou=" + ou + ",");
		}

		sb.append(baseUserDN);

		return sb.toString();
	}

	/**
	 * 以防万一
	 */
	protected void finalize() {
		this.closeDirContext();
	}

	// 设置属性
	private void putAttribute(Attributes attrs, String attrName,
			Object attrValue) {
		if (attrValue != null && attrValue.toString().length() != 0) {
			Attribute attr = new BasicAttribute(attrName, attrValue);
			attrs.put(attr);
		}
	}

	private Attributes addObjclassAttrs(Attributes attrs) {
		Attribute objclass = new BasicAttribute("objectclass");
		objclass.add("top");
		objclass.add("person");
		objclass.add("organizationalPerson");
		objclass.add("user");
		attrs.put(objclass);
		return attrs;
	}

	/**
	 * 下面这些是用户中LCS的属性:
	 * <p>
	 * msRTCSIP-ArchivingEnabled 1 开启存档<br>
	 * msRTCSIP-FederationEnabled FALSE <br>
	 * msRTCSIP-InternetAccessEnabled FALSE<br>
	 * msRTCSIP-OptionFlags 0 此属性在老版本的lcs2005中不存在<br>
	 * msRTCSIP-PrimaryHomeServer<br>
	 * CN=LC Services,CN=Microsoft,CN=LCS182,CN=Pools,CN=RTC
	 * Service,CN=Microsoft,CN=System,DC=bxxk,DC=com,DC=cn<br>
	 * msRTCSIP-PrimaryUserAddress sip:lcs1@bxxk.com.cn lcs的帐户名称<br>
	 * msRTCSIP-UserEnabled TRUE 是否启用lcs帐户<br>
	 */
	private Attributes getCommonUserAttrs(ADUserInfo user) throws Exception {
		Attributes attrs = new BasicAttributes();

		// 设置属性
		putAttribute(attrs, "userPrincipalName", user.getSamAccountName()
				+ domain);
		putAttribute(attrs, "sAMAccountName", user.getSamAccountName());

		if (StringUtils.isNotEmpty(user.getGivenName())) {
			putAttribute(attrs, "mail", user.getEmail());
		}

		if (StringUtils.isNotEmpty(user.getGivenName())) {
			putAttribute(attrs, "givenname", user.getGivenName());
		}
		if (StringUtils.isNotEmpty(user.getSn())) {
			putAttribute(attrs, "sn", user.getSn());
		}
		if (StringUtils.isNotEmpty(user.getDescription())) {
			putAttribute(attrs, "description", user.getDescription());
		}
		if (StringUtils.isNotEmpty(user.getCompany())) {
			putAttribute(attrs, "company", user.getCompany());
		}
		if (StringUtils.isNotEmpty(user.getDepartment())) {
			putAttribute(attrs, "department", user.getDepartment());
		}
		if (StringUtils.isNotEmpty(user.getDisplayName())) {
			putAttribute(attrs, "displayName", user.getDisplayName());
		}
		if (StringUtils.isNotEmpty(user.getPassword())) {
			putAttribute(attrs, "unicodePwd",
					("\"" + user.getPassword() + "\"").getBytes("UTF-16LE"));
		}

		if ("true".equalsIgnoreCase(enabledLCS) || "on".equals(enabledLCS)) {
			putAttribute(attrs, "msRTCSIP-ArchivingEnabled", "1");
			putAttribute(attrs, "msRTCSIP-FederationEnabled", "FALSE");
			putAttribute(attrs, "msRTCSIP-InternetAccessEnabled", "FALSE");
			// putAttribute(attrs, "msRTCSIP-OptionFlags", "0");
			putAttribute(attrs, "msRTCSIP-PrimaryHomeServer", LCServerDN);
			putAttribute(attrs, "msRTCSIP-PrimaryUserAddress",
					"sip:" + user.getSamAccountName() + domain);
			putAttribute(attrs, "msRTCSIP-UserEnabled", "TRUE");
		}

		return attrs;
	}
	// ------------------------------------------------------------------------END
	// 以下都是私有成员, 工具函数
	// ------------------------------------------------------------------------END
}
